# 第十一章 后端物流管理
# 订单物流状态查询
通过前面小节的学习我们实现了订单发货的操作,在测试过程中我们给接口提交了一些真实有效的快递单信息,本小节我们就将利用这些信息来实现订单物流状态的查询,这也是一个很实用的后台管理功能,经常网购的小伙伴应该深有体会,我们常常会因为一些物流上的问题找到平台方,平台方就会要你提供订单号或者快递单号,然后告诉你快递现在是xxxx状态了等等。要实现物流查询,快递单号我们有了,那么怎么查呢?现在互联网上有很多平台都有提供快递信息查询的开放接口,我们通过调用这些平台的接口就可以实现快递信息的查询,这里我们选用聚合数据
平台下的常用快递
接口,点击访问:聚合数据-常用快递查询,首先申请使用赠送100次免费调用,这里用于我们测试学习基本足够,后续如果读者有业务方面的需要可以续费充值或者尝试寻找其他平台的开放接口。点击立即申请会提示你要登录账号,大家自己注册一个账号即可,注册并登陆完成之后会有个确认申请的页面,点击立即申请即可。申请成功之后,在网站个人中心里左侧菜单栏中找到并点击数据中心——我的接口,会看到:
这里我们的常用快递查询的接口已经申请成功,注意红框位置AppKey的内容,复制下来,一会我们会用到。接口申请到了,那如何调用呢,我们可以回到刚刚申请接口的页面,页面上就有API文档和各种语言的示例代码,但这里我们并不打算手动实现相关对接的代码,因为每个平台的调用方式、接口地址不一样,如果哪天这个平台跑路了或者我们不想用了,那就得重新对接其他平台的接口,有没有什么轮子可以同时支持多个平台的快递查询接口,我们只需要提供对平台的AppKey即可呢?答案是有的,这里我们选用了GitHub上的一个开源项目finecho/logistics,这是一个支持阿里云、聚合数据、快递100、快递鸟四个平台快递查询的扩展包,我们只需要分别去各个平台申请接口,然后把相关的身份标识(如AppKey)配置好并在调用时传递即可,使用的过程相当简单。接下来我们就来安装一下这个扩展包,在lin-cms-tp5项目根目录下,使用命令行工具执行一下命令(更推荐使用PHP Storm打开项目,然后按Alt+F12在IDE中打开命令行工具,可以少开一个窗口):
composer require finecho/logistics -vvv
经过一阵等待之后,我们的扩展就安装好了,然后就可以在我们的项目中使用了。还是老套路,我们需要先定义一个控制器方法,在控制层下,新增一个Logistics
控制器类并在控制器类里新增一个getLogistics()
方法:
<?php
namespace app\api\controller\v1;
class Logistics
{
public function getLogistics($orderNo)
{
}
}
2
3
4
5
6
7
8
9
10
11
12
13
控制器方法接收一个$orderNo参数,即订单号,我们需要根据订单号去订单发货记录表中查询到指定的记录,然后根据记录里面的快递单号去发起一个快递状态的查询,这里我们把这个快递查询的逻辑封装成一个方法,打开service
服务层下的order
服务类,新增一个方法queryLogistics()
:
<?php
namespace app\api\service;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\api\model\Order as OrderModel;
use app\lib\enum\OrderStatusEnum;
use app\lib\exception\order\OrderException;
use app\lib\token\Token;
use Finecho\Logistics\Logistics;
use think\Db;
use think\Exception;
class Order
{
..........................................
..........................................
public static function queryLogistics($orderNo)
{
$deliverRecord = DeliverRecordModel::where('order_no', $orderNo)->find();
if (!$deliverRecord) {
Throw new OrderException(['msg' => '未查询到指定订单号发货单记录', 'error_code' => 70011]);
}
// 查询缓存中是否有该快递单号的快递信息
$cache = cache($deliverRecord->comp . $deliverRecord->number);
// 如果有,直接返回缓存中的信息
if ($cache) return $cache;
// 如果不存在,调用第三方扩展进行快递查询
// 获取第三方扩展需要的配置信息
$config = config('logistics.config');
// 获取快递编码对应公司名称
$comp = config('logistics.comp')[$deliverRecord->comp];
// 实例化第三方扩展类并调用query查询方法,第一个参数是快递单号,第二个参数是快递公司名称(可选,但推荐传递)
try {
$logisticsOrder = (new Logistics($config))->query($deliverRecord->number, $comp);
// 查询成功后把查询结果缓存起来,保留1200秒,即20分钟,这个缓存的过期时间可以按自己需要设置
cache($deliverRecord->comp . $deliverRecord->number, $logisticsOrder['list'], 1200);
// 返回查询结果
return $logisticsOrder['list'];
} catch (\Finecho\Logistics\Exceptions\Exception $ex) {
throw new OrderException(['msg' => $ex->getMessage(), 'error_code' => 70012]);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
由于这个方法不需要调用到订单查询,所以这里我们把这个方法定义成静态的方法,可以免去控制层实例化的过程。方法接收控制层获取到的$orderNo参数,首先我们会根据这个$orderNo查询是否存在发货单记录,如果没有就给个异常。接着我们判断一下在缓存中有没有名字为快递编码+快递单号
的缓存,如果有就直接返回缓存中的数据,如果没有继续执行下面的逻辑。(new Logistics($config))->query($deliverRecord->number, $comp);
就是调用扩展查询快递信息的语句,比起我们自己手动实现要简单得多,有轮子就是好对吧。Logistics在实例化时会要求传入一个配置信息,这里我们通过TP内置的助手函数获取了名叫logistics
配置文件下的config
属性的值,当然这个配置文件目前我们是没有的,在项目根目录下的config目录下新建一个logistics.php文件,并加入以下内容:
<?php
// +----------------------------------------------------------------------
// | 快递查询接口配置
// +----------------------------------------------------------------------
return [
'config' => [
'provider' => 'juhe',
'juhe' => [
'app_code' => '填写你的AppKey',
],
],
'comp' => [
'sf' => '顺丰',
'sto' => '申通',
'yt' => '圆通',
'yd' => '韵达',
'tt' => '天天',
'ems' => 'EMS',
'zto' => '中通',
'ht' => '汇通',
'qf' => '全峰',
'db' => '德邦',
'gt' => '国通',
'rfd' => '如风达',
'jd' => '京东',
'zjs' => '宅急送',
'youzheng' => '邮政快递',
'bsky' => '百世',
]
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
在配置文件中,我们返回了一个二维数组,config
里包含了扩展包里Logistics类实例化时所需的必要参数,其中app_code
请填写你自己申请接口成功后平台下发给你的AppKey;comp
是快递公司编码和对应名称的数组,后面我们需要根据订单发货记录里的快递公司编码来找对应的公司名称,这里读者可能会问,为什么不一开始就直接在发货记录表里记录公司名称,这是因为每个平台的快递查询接口对快递公司名称的定义不一样而且也不是固定不变的,如果直接在数据表中记录公司名称,如果哪天我们更换了平台接口,公司名称对不上了,就得逐个去改表记录中的字段值。只记录公司编码的好处就是如果公司名称有变化,我们只需要修改一下这个配置文件里的内容即可。定义完配置文件之后,让我们回到服务层下的queryLogistics()
方法:
<?php
namespace app\api\service;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\api\model\Order as OrderModel;
use app\lib\enum\OrderStatusEnum;
use app\lib\exception\order\OrderException;
use app\lib\token\Token;
use Finecho\Logistics\Logistics;
use think\Db;
use think\Exception;
class Order
{
..........................................
..........................................
public static function queryLogistics($orderNo)
{
$deliverRecord = DeliverRecordModel::where('order_no', $orderNo)->find();
if (!$deliverRecord) {
Throw new OrderException(['msg' => '未查询到指定订单号发货单记录', 'error_code' => 70011]);
}
// 查询缓存中是否有该快递单号的快递信息
$cache = cache($deliverRecord->comp . $deliverRecord->number);
// 如果有,直接返回缓存中的信息
if ($cache) return $cache;
// 如果不存在,调用第三方扩展进行快递查询
// 获取第三方扩展需要的配置信息
$config = config('logistics.config');
// 获取快递编码对应公司名称
$comp = config('logistics.comp')[$deliverRecord->comp];
// 实例化第三方扩展类并调用query查询方法,第一个参数是快递单号,第二个参数是快递公司名称(可选,但推荐传递)
try {
$logisticsOrder = (new Logistics($config))->query($deliverRecord->number, $comp);
// 查询成功后把查询结果缓存起来,保留1200秒,即20分钟,这个缓存的过期时间可以按自己需要设置
cache($deliverRecord->comp . $deliverRecord->number, $logisticsOrder['list'], 1200);
// 返回查询结果
return $logisticsOrder['list'];
} catch (\Finecho\Logistics\Exceptions\Exception $ex) {
throw new OrderException(['msg' => $ex->getMessage(), 'error_code' => 70012]);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
准备好调用扩展方法的几个必须的参数之后,我们就可以来正式感受一下这个扩展的功能了,按照扩展说明文档的使用方法介绍,这里我们把几个必要的参数传递进去,并用一个$logisticsOrder变量来接收返回结果,在接收到结果之后,调用TP内置的缓存操作助手函数,以快递公司编码+快递单号
为名称,把查询结果缓存起来,并设置一个缓存的超时时间,最后把查询结果返回出去。
这里之所以使用缓存是因为我们的接口调用次数是有限的(续费死贵死贵的),而且快递运输过程中的每个环节都有其需要等待的时间,没必要频繁的调用平台的接口,特别是当某一个时间段内多个人查询同一个快递单的时候。
这里我们还用了一个try/catch语句包裹了调用过程,在查询发生异常时,捕获异常信息并以我们自定义异常的形式抛出去。这里为什么作者会知道这里会有异常的可能?没有为什么,一个是看源码,一个是试出来的,我们在使用第三方扩展包的时候,除了需要仔细阅读相关文档以外,测试是必不可少的,因为你不知道这个扩展会不会有些BUG或者一些特殊行为。这里catch要捕获的异常对象\Finecho\Logistics\Exceptions\Exception
也是作者通过阅读源码梳理了下异常的逻辑才知道的(刚开始用的时候没成功捕获到异常)。在实现了服务层的功能之后,我们就可以在控制层中调用了,回到控制层中Logistics
控制器类下的getLogistics()
方法:
<?php
namespace app\api\controller\v1;
use app\api\service\Order as OrderService;
class Logistics
{
/**
* 查询订单物流状态
* @param('orderNo','订单号','require|length:16|alphaNum')
*/
public function getLogistics($orderNo)
{
$result = OrderService::queryLogistics($orderNo);
return $result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
由于是静态方法,这里我们直接使用::
调用queryLogistics()方法并把订单号传递进去并用一个$result变量接收结果然后返回给前端,这里我们顺手给$orderNo参数加了一个注解参数校验。定义完控制器方法之后,让我们打开route.php
,在v1
路由分组下新增一个logistics
路由分组,并在该分组下新增一条路由规则:
Route::group('', function () {
Route::group('cms', function () {
// CMS管理相关的路由规则
// 内容省略。。。。
});
Route::group('v1', function () {
// 业务接口相关的路由规则
............................
............................
............................
............................
// 订单管理相关接口
Route::group('order', function () {...});
// 物流管理相关接口
Route::group('logistics', function () {
// 分页查询所有订单
Route::get(':orderNo', 'api/v1.Logistics/getLogistics');
});
});
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:
由于平台支持原因,顺丰快递查询可能存在无法查询的情况。
这里作者尝试查询一个已经发货了的订单号,点击发送:
[
{
"datetime": "2019-06-11 18:28:58",
"remark": "浙江苍南公司-已发往-浙江温州转运中心[发件]",
"zone": ""
},
{
"datetime": "2019-06-11 18:28:58",
"remark": "浙江苍南公司-已进行装袋扫描[装袋]",
"zone": ""
},
{
"datetime": "2019-06-11 18:30:47",
"remark": "浙江苍南公司-已进行装车扫描[装车]",
"zone": ""
},
// 省略部分内容
.................................................
.................................................
.................................................
{
"datetime": "2019-06-13 09:59:08",
"remark": "广东广州时代分部-邓平-派件中[派件]",
"zone": ""
},
{
"datetime": "2019-06-13 12:58:41",
"remark": "已签收",
"zone": ""
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
没有报错,这里返回了商品订单对应的快递信息,这说明我们使用的第三方扩展包是可以正常工作的。这里读者可能会好奇,在服务层中的实现里,我们有用到缓存这种技术,我怎么知道当前的查询是调用平台接口得到的还是从缓存中获取的,要知道这个答案方法有很多种,最简单的就是你每调用一次,就去平台的个人中心里看看你接口的剩余调用次数有没有减少(这个回答肯定满足不了你),另一种方法是观察Postman在返回接口数据后记录的本次请求耗时,如果是调用平台的接口,因为涉及到和其他平台接口的交互(扩展包里会实现发起一个HTTP请求指定的平台),耗时会比较长。如果是从缓存中读取,则会快很多。当然,这里更推荐的一种做法是使用TP内置的dump()
助手函数,dump()是对PHP原生var_dump()
的封装,本质上都是输出变量信息,只不过dump()输出的结果看起来更直观些,比如在前面我们服务层的实现中,我们想看看到底有没有走缓存,我们可以这样做:
<?php
namespace app\api\service;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\api\model\Order as OrderModel;
use app\lib\enum\OrderStatusEnum;
use app\lib\exception\order\OrderException;
use app\lib\token\Token;
use Finecho\Logistics\Logistics;
use think\Db;
use think\Exception;
class Order
{
..........................................
..........................................
public static function queryLogistics($orderNo)
{
...........................................
...........................................
// 如果有,直接返回缓存中的信息
if ($cache) return $cache;
dump('没有缓存');
// 如果不存在,调用第三方扩展进行快递查询
.........................................
.........................................
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
这里我们在没有缓存的情况下调用一下dump()助手函数,输出一个字符串,然后再来请求下接口:
注意这里返回结果多出一个字符串信息,说明这个结果不是从缓存中返回的,接着我们再调用一次接口:
因为有了上一次的查询,结果会被缓存起来,所以刚刚那个字符串信息不见了,返回的数据也恢复成了正常的json格式数据,这就说明我们的数据是从缓存中返回的。
扩展知识:
这里只是利用dump函数小试牛刀而已,在真实开发场景中,像这种打印变量信息来debug是很常见的操作。很多时候我们在实现一个稍微复杂的逻辑的时候,我们希望知道每一步或者每个代码块的执行结果,这时候dump函数就非常有用,当然debug的手段不局限于使用dump函数,还有通过安装xdebug插件实现断点调试的。但比起使用什么debug工具,更重要的还是debug的意识和技巧。意识在这里是指当我们的代码出现了不符合预期的效果或者异常中断了的时候,我们首先第一步是要梳理你这次运行的大概流程是什么,这里可能读者会说,我水平还达不到可以梳理一个功能运行流程的境界,我想说的是,你不是想学习么?这就是一个很棒的学习机会,因为你会遇到很多知识盲区,尝试去逐一攻克吧!自己尝试一下在逐个环节debug断点调试或者打印下变量信息,然后顺藤摸瓜,必要时再借助搜索引擎,问题则基本可以定位并解决了。所以,当程序运行出现问题时,正确的做法是自己动手debug和利用搜索引擎,强烈不推荐QQ群或微信群喊救命,这是最低效也是最没有价值的方式,这种方式仅适合于山穷水尽之时;至于技巧,没有技巧,就是多debug,有时候同一个问题,A能很快定位到问题,B需要花费一定时间定位问题,这里面的差别就在于经验的积累,就好比作者在做项目管理,有些工作任务本身有固有章法和套路,但有一定经验积累之后,完全可以不按套路来也能直接达到最终效果。最后总结一句就是,debug是一门学问,也是程序员实力体现的重要指标之一。
# 分页查询所有发货记录
通过前面的学习,我们实现了订单发货和订单物流状态的查询,这两个查询都会依赖于deliver_record
发货记录表,这个表除了为相关查询提供参数以外,还有个功能就是为内部审计或者定位一些问题时提供支持,所以我们需要提供一个接口,通过调用这个接口我们能查询到所有的订单发货记录,打开项目控制层下的Order
控制器类,新增一个getOrderDeliverRecord()
方法:
<?php
namespace app\api\controller\v1;
use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\lib\exception\order\OrderException;
use think\facade\Request;
class Order
{
/**分页查询所有订单*/
public function getOrders(){...}
/**订单发货*/
public function deliverGoods($id){...}
/**
* 分页查询订单发货记录
* @validate('DeliverRecordForm')
*/
public function getOrderDeliverRecord()
{
$params = Request::get();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
这里我们首先给控制器方法加上一个注解验证器,调用DeliverRecordForm
验证器类来进行参数校验,这里我们需要创建一下这个验证器类,在项目的验证层下的order
目录下新建一个DeliverRecordForm类,并添加如下代码:
<?php
namespace app\api\validate\order;
use LinCmsTp5\validate\BaseValidate;
class DeliverRecordForm extends BaseValidate
{
protected $rule = [
'page' => 'require|number',
'count' => 'require|number|between:1,15',
'order_no' => 'length:16|alphaNum',
'number' => 'alphaNum',
'operator' => 'chsAlphaNum'
];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
通过校验规则可以猜到,我们这个接口同样会是分页查询的实现方式。后面的3个参数属于可选参数,用来做查询的过滤条件。既然是分页查询,我们就要在模型中封装一个分页查询的方法了,打开模型层下的DeliverRecord
模型类,新增一个getDeliverRecordPaginate()
方法,并添加如下代码:
<?php
namespace app\api\model;
class DeliverRecord extends BaseModel
{
public $autoWriteTimestamp = true;
protected $hidden = ['update_time'];
public static function getDeliverRecordPaginate($params)
{
// 需要判断是否存在的参数名
$field = ['order_no', 'number', 'operator'];
// 构造数组查询条件
$query = self::equalQuery($field, $params);
// paginate()方法用于根据url中的参数,计算查询要查询的开始位置和查询数量
list($start, $count) = paginate();
// 应用条件查询
$courierOrderList = self::where($query);
// 调用模型的实例方法count计算该条件下会有多少条记录
$totalNums = $courierOrderList->count();
// 调用模型的limit方法对记录进行分页并获取查询结果
$orderList = $courierOrderList->limit($start, $count)
->order('create_time desc')
->select();
// 组装返回结果
$result = [
'collection' => $orderList,
'total_nums' => $totalNums
];
return $result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
DeliverRecord模型的分页查询查询方法与之前我们定义其他分页查询没有任何区别,同时这里我们再次复用了之前我们封装好的equalQuery
方法用于构造数组查询条件。模型方法定义好之后,我们就可以到控制层中调用一下了,回到Order
控制器类下的getOrderDeliverRecord()
方法:
<?php
namespace app\api\controller\v1;
use app\api\model\Order as OrderModel;
use app\api\service\Order as OrderService;
use app\api\model\DeliverRecord as DeliverRecordModel;
use app\lib\exception\order\OrderException;
use think\facade\Request;
class Order
{
/**分页查询所有订单*/
public function getOrders(){...}
/**订单发货*/
public function deliverGoods($id){...}
/**
* 分页查询订单发货记录
* @validate('DeliverRecordForm')
*/
public function getOrderDeliverRecord()
{
$params = Request::get();
$result = DeliverRecordModel::getDeliverRecordPaginate($params);
if ($result['total_nums'] === 0) {
throw new OrderException([
'code' => 404,
'msg' => '未查询到相关发货记录',
'error_code' => '70010'
]);
}
return $result;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
这里我们在控制器方法中调用了我们刚刚封装好的模型方法,接着来给这个控制器方法定义一条路由,打开route.php
,在order
路由分组下新增一条路由规则:
Route::group('', function () {
Route::group('cms', function () {
// CMS管理相关的路由规则
// 内容省略。。。。
});
Route::group('v1', function () {
// 业务接口相关的路由规则
............................
............................
............................
............................
Route::group('order', function () {
// 分页查询所有订单
Route::get('', 'api/v1.Order/getOrders');
// 订单发货
Route::post('shipment/:id', 'api/v1.Order/deliverGoods');
// 查询发货记录
Route::get('shipment/record', 'api/v1.Order/getOrderDeliverRecord');
});
});
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
路由定义完了之后打开Postman,按照路由信息新增并配置一个请求:
点击发送:
{
"collection": [
{
"id": 8,
"order_no": "C903906920662485",
"comp": "sf",
"number": "89764564354",
"operator": "super",
"create_time": "2019-09-06 21:07:16"
},
{
"id": 7,
"order_no": "C902910033532723",
"comp": "sf",
"number": "65989835651121354",
"operator": "super",
"create_time": "2019-09-06 16:35:33"
},
{
"id": 6,
"order_no": "C902911669242605",
"comp": "sf",
"number": "7758885",
"operator": "super",
"create_time": "2019-09-05 23:18:54"
},
{
"id": 5,
"order_no": "C902910033532723",
"comp": "sf",
"number": "7758885",
"operator": "super",
"create_time": "2019-09-05 23:03:49"
},
{
"id": 4,
"order_no": "C902203104786342",
"comp": "ems",
"number": "775333335",
"operator": "super",
"create_time": "2019-09-05 20:49:48"
}
],
"total_nums": 8
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
没有报错,这里返回了之前我们操作订单的记录,这里读者可以自行给这个请求加入一些查询条件来查询指定的发货记录。
# 章节回顾
本章节我们主要学习了如何利用第三方接口来实现我们的业务需求,同时在这个基础上,通过轮子(扩展包)
来简化我们对第三方接口的调用。找轮子和用轮子在我们实际开发中也是经常会干的事情,我们要学会站在巨人的肩膀上,码出效率,码出质量。如果想进一步提升自己,就需要从用轮子的人变成造轮子的人了。当然使用轮子时也要注意一些可能存在的坑,比如说轮子本身的优缺点、稳定性和持续维护性,这些都可以通过观察轮子所在的Github仓库页面的Issues、Star数量和最近提交日期来观察得到。
- 上一页
- 首页
- 1
- 尾页
- 下一页
- 总共1页